/*******************************************************************************
 * Copyright (c) 2011 LegSem.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.txt
 * 
 * Contributors:
 *     LegSem - initial API and implementation
 ******************************************************************************/
package com.legstar.eci.xsd;

import java.io.IOException;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaMaxInclusiveFacet;
import org.apache.ws.commons.schema.XmlSchemaMinInclusiveFacet;
import org.apache.ws.commons.schema.XmlSchemaObject;
import org.apache.ws.commons.schema.XmlSchemaObjectCollection;
import org.apache.ws.commons.schema.XmlSchemaSimpleType;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeContent;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeRestriction;
import org.apache.ws.commons.schema.XmlSchemaType;
import org.w3c.dom.Element;

import com.legstar.coxb.CobolMarkup;
import com.legstar.coxb.CobolType;
import com.legstar.dom.ElementFactory;
import com.legstar.eci.xmi.model.AggregateInstanceTD;
import com.legstar.eci.xmi.model.SimpleInstanceTD;
import com.legstar.eci.xmi.model.TypeDescriptorMap;
import com.legstar.eci.xmi.model.WsdlBinding;
import com.legstar.eci.xmi.model.XMI;
import com.legstar.xsd.AbstractXsdAnnotator;
import com.legstar.xsd.XsdConstants;
import com.legstar.xsd.XsdMappingException;

/**
 * Procudes COBOL annotations based on ECI XMI WSDL binding data.
 * <p/>
 * The structure is identical to the ECI XSD and types are derived from the ECI
 * meta data.
 * 
 */
public class EciXsdAnnotator extends AbstractXsdAnnotator {

    /** Logging. */
    private Log _log = LogFactory.getLog(getClass());

    /** The ECI WSDL binding model. */
    private WsdlBinding _wsdlBinding;

    /** The ECI XMI object. */
    private XMI _xmi;

    /**
     * Construct using ECI WSDL binding.
     * 
     * @param wsdlBinding holds the ECI WSDL binding meta data
     */
    public EciXsdAnnotator(final WsdlBinding wsdlBinding) {
        super();
        _wsdlBinding = wsdlBinding;
    }

    /** {@inheritDoc} */
    @Override
    public void setUp() throws IOException {
        super.setUp();
        if (_wsdlBinding == null) {
            throw new IOException("No WSDL binding provided");
        }
        if (_wsdlBinding.getPhysicalFormats() == null
                || _wsdlBinding.getPhysicalFormats().size() == 0) {
            throw new IOException(
                    "WSDL binding does not contain physical formats");
        }
        if (_wsdlBinding.getPhysicalFormats().get(0).getXmi() == null
                || _wsdlBinding.getPhysicalFormats().get(0).getXmi().size() == 0) {
            throw new IOException(
                    "WSDL binding does not contain an XMI element");
        }
        _xmi = _wsdlBinding.getPhysicalFormats().get(0).getXmi().get(0);
    }

    /** {@inheritDoc} */
    public void processElement(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final int level)
            throws XsdMappingException {
        if (_log.isDebugEnabled()) {
            _log.debug("setAttributes started for element  = "
                    + xsdElement.getName());
            _log.debug("   XmlSchemaElement QName          = "
                    + xsdElement.getQName());
            _log.debug("   XmlSchemaElement SchemaType     = "
                    + xsdElement.getSchemaType());
            _log.debug("   XmlSchemaElement SchemaTypeName = "
                    + xsdElement.getSchemaTypeName());
            _log.debug("   XmlSchemaElement MaxOccurs      = "
                    + xsdElement.getMaxOccurs());
            _log.debug("   XmlSchemaElement MinOccurs      = "
                    + xsdElement.getMinOccurs());
            _log.debug("   XmlSchemaElement RefName        = "
                    + xsdElement.getRefName());
            _log.debug("   XmlSchemaElement DefaultValue   = "
                    + xsdElement.getDefaultValue());
            _log.debug("   XmlSchemaElement FixedValue     = "
                    + xsdElement.getFixedValue());
        }

        String coxbPrefix = schema.getNamespaceContext().getPrefix(
                CobolMarkup.NS);
        Element elc = ElementFactory.createElement(CobolMarkup.NS, coxbPrefix
                + ":" + CobolMarkup.ELEMENT);

        /* Add cobol attributes valid for all types */
        elc.setAttribute(CobolMarkup.LEVEL_NUMBER, Integer.toString(level));
        elc.setAttribute(CobolMarkup.COBOL_NAME,
                getCobolName(xsdElement.getName()));

        if (_log.isDebugEnabled()) {
            _log.debug("   Cobol level          = " + level);
            _log.debug("   Cobol name           = "
                    + elc.getAttribute(CobolMarkup.COBOL_NAME));
        }
        /*
         * The semantic for maxOccurs is different for Cobol annotations than
         * for XML Schema. a maxOccurs of 1 is a one item array for Cobol which
         * is different from a simple item. If schema maxOccurs=1 we do not
         * insert a Cobol maxOccurs annotation at all.
         */
        /*
         * Here we assume ECI did set the XML schema minOccurs and maxOccurs
         * accurately. Alternatively, we could check the ArrayDescr optional
         * object on the instance ID. It contains lowerBound and upperBound.
         * TODO: Is there support for variable size arrays in ECI?
         */
        if (xsdElement.getMaxOccurs() > 1) {

            elc.setAttribute(CobolMarkup.MAX_OCCURS,
                    Long.toString(xsdElement.getMaxOccurs()));

            elc.setAttribute(CobolMarkup.MIN_OCCURS,
                    Long.toString(xsdElement.getMinOccurs()));

            if (_log.isDebugEnabled()) {
                _log.debug("   Cobol minOccurs      = "
                        + elc.getAttribute(CobolMarkup.MIN_OCCURS));
                _log.debug("   Cobol maxOccurs      = "
                        + elc.getAttribute(CobolMarkup.MAX_OCCURS));
            }
        }

        /* Use ECI XMI meta data to set COBOL attributes */
        setAttributesFromEci(schema, xsdElement, elc, level);

        /* Add the annotation to the XML schema object */
        annotate(schema, xsdElement, elc);

        if (_log.isDebugEnabled()) {
            _log.debug("setAttributes ended for element = "
                    + xsdElement.getName());
        }

    }

    /**
     * Using ECI meta data we infer a COBOL type for an XML schema element.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param level teh COBOL level
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setAttributesFromEci(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final int level) throws XsdMappingException {

        if (_log.isDebugEnabled()) {
            _log.debug("searching ECI for XML schema element: "
                    + xsdElement.getQName());
        }

        /*
         * Elements that were artificially injected in the XML schema have no
         * XMI physical representation. If this is a complex type, we handle it
         * here. If it is not, it means it was injected by someone else and we
         * make no attempt at handling it.
         */
        if (xsdElement.getSourceURI() != null
                && xsdElement.getSourceURI().equals(
                        XsdConstants.INJECTED_ELEMENT)) {
            if (xsdElement.getSchemaType() instanceof XmlSchemaComplexType) {
                setComplexTypeAttributes(schema,
                        (XmlSchemaComplexType) xsdElement.getSchemaType(), elc,
                        level);
            } else {
                _log.warn("Injected element " + xsdElement.getQName()
                        + " is not a complex type. It will be ignored");
            }
            return;
        }

        /*
         * There must be a TypeDescriptorMap in the ECI WSDL binding that points
         * (via href) to the XML schema element we are processing. ECI's href is
         * too complicated to recreate completely. FIXME We make the
         * questionable assumption here that elements have unique qualified
         * names within the XML schema.
         */

        String instanceTD = null;
        for (TypeDescriptorMap tdm : _xmi.getTypeDescriptorMaps()) {
            String href = tdm.getType().getHref();
            if (href != null
                    && href.contains(xsdElement.getQName().getNamespaceURI())
                    && href.contains("/" + xsdElement.getQName().getLocalPart()
                            + ";XSDElementDeclaration")) {
                instanceTD = tdm.getInstanceTD();
                if (_log.isDebugEnabled()) {
                    _log.debug("found TypeDescriptorMap href: " + href);
                }
                break;
            }
        }
        if (instanceTD == null) {
            throw new XsdMappingException("XML schema element "
                    + xsdElement.getQName() + " cannot be located in "
                    + _wsdlBinding.getName());
        }

        /*
         * We now have an aggregate or simple instance ID. So we lookup the
         * corresponding model object.
         */

        if (_log.isDebugEnabled()) {
            _log.debug("searching ECI for instanceTD: " + instanceTD);
        }

        SimpleInstanceTD simpleInstanceTD = null;
        for (SimpleInstanceTD s : _xmi.getSimpleInstanceTDs()) {
            if (s.getId().equals(instanceTD)) {
                simpleInstanceTD = s;
                break;
            }
        }
        if (simpleInstanceTD != null) {
            setSimpleTypeAttributes(schema, xsdElement, elc, simpleInstanceTD);
            return;
        }

        AggregateInstanceTD aggregateInstanceTD = null;
        for (AggregateInstanceTD s : _xmi.getAggregateInstanceTDs()) {
            if (s.getId().equals(instanceTD)) {
                aggregateInstanceTD = s;
                break;
            }
        }
        if (aggregateInstanceTD != null) {
            XmlSchemaType xsdType = xsdElement.getSchemaType();
            if (xsdType instanceof XmlSchemaComplexType) {
                setComplexTypeAttributes(schema,
                        (XmlSchemaComplexType) xsdType, elc, level);
            } else {
                throw new XsdMappingException(
                        "Inconsistent ECI type for XML schema element "
                                + xsdElement.getQName() + " in "
                                + _wsdlBinding.getName());
            }
            return;
        }

        throw new XsdMappingException(
                "Unable to find ECI type for XML schema element "
                        + xsdElement.getQName() + " in "
                        + _wsdlBinding.getName());

    }

    /**
     * Based on an element ECI type, we route to a specialized attribute setter.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param simpleInstanceTD the ECI instance Type Descriptor
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setSimpleTypeAttributes(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final SimpleInstanceTD simpleInstanceTD) throws XsdMappingException {

        String sharedType = simpleInstanceTD.getSharedType();

        /* Determine the element ECI type */
        String eciType = getEciType(sharedType);
        if (eciType.equals(EciXsdConstants.ECI_STRING_TYPE)) {
            setAlphaNumericAttributes(schema, xsdElement, elc, simpleInstanceTD);
        } else if (eciType.equals(EciXsdConstants.ECI_PACKED_DECIMAL_TYPE)) {
            setPackedDecimalAttributes(schema, xsdElement, elc,
                    simpleInstanceTD);
        } else if (eciType.equals(EciXsdConstants.ECI_INTEGER_TYPE)) {
            setIntegerAttributes(schema, xsdElement, elc, simpleInstanceTD);
        } else if (eciType.equals(EciXsdConstants.ECI_EXTERNAL_DECIMAL_TYPE)) {
            setExternalDecimalAttributes(schema, xsdElement, elc,
                    simpleInstanceTD);
        } else {
            throw new XsdMappingException("Unrecognized EXCI XMI type "
                    + eciType);
        }

    }

    /**
     * Sets attributes for a String.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param simpleInstanceTD the ECI instance Type Descriptor
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setAlphaNumericAttributes(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final SimpleInstanceTD simpleInstanceTD) throws XsdMappingException {

        if (_log.isDebugEnabled()) {
            _log.debug("setAlphaNumericAttributes started for element = "
                    + xsdElement.getQName());
        }

        elc.setAttribute(CobolMarkup.TYPE,
                CobolType.ALPHANUMERIC_ITEM.toString());
        elc.setAttribute(CobolMarkup.USAGE, "COMP-3");

        /*
         * TODO make sure the size is what we need (double byte) Other
         * candidates are: contentSize in SimpleInstanceTD width in StringTD
         * Also might be important to look at: characterSize in StringTD
         */
        elc.setAttribute(CobolMarkup.PICTURE, "X(" + simpleInstanceTD.getSize()
                + ")");
        elc.setAttribute(CobolMarkup.USAGE, "DISPLAY");

        if (_log.isDebugEnabled()) {
            _log.debug("setAlphaNumericAttributes ended for element = "
                    + xsdElement.getQName());
            _log.debug("   Cobol picture        = "
                    + elc.getAttribute(CobolMarkup.PICTURE));
            _log.debug("   Cobol usage          = "
                    + elc.getAttribute(CobolMarkup.USAGE));
        }
    }

    /**
     * Sets attributes for a Packed Decimal.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param simpleInstanceTD the ECI instance Type Descriptor
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setPackedDecimalAttributes(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final SimpleInstanceTD simpleInstanceTD) throws XsdMappingException {

        elc.setAttribute(CobolMarkup.TYPE,
                CobolType.PACKED_DECIMAL_ITEM.toString());
        elc.setAttribute(CobolMarkup.USAGE, "COMP-3");

        /* Set defaults just in case facets are not found */
        int totalDigits = 2 * simpleInstanceTD.getSize() - 1;
        elc.setAttribute(CobolMarkup.TOTAL_DIGITS,
                Integer.toString(totalDigits));
        elc.setAttribute(CobolMarkup.FRACTION_DIGITS, "0");
        elc.setAttribute(CobolMarkup.IS_SIGNED, "true");

        setCommonNumericAttributes(xsdElement, elc);

    }

    /**
     * Sets attributes for an Integer.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param simpleInstanceTD the ECI instance Type Descriptor
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setIntegerAttributes(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final SimpleInstanceTD simpleInstanceTD) throws XsdMappingException {

        elc.setAttribute(CobolMarkup.TYPE, CobolType.BINARY_ITEM.toString());
        elc.setAttribute(CobolMarkup.USAGE, "COMP");

        /* Set defaults just in case facets are not found */
        int totalDigits = (simpleInstanceTD.getSize() < 3) ? 4
                : ((simpleInstanceTD.getSize() < 5) ? 9 : 19);
        elc.setAttribute(CobolMarkup.TOTAL_DIGITS,
                Integer.toString(totalDigits));
        elc.setAttribute(CobolMarkup.FRACTION_DIGITS, "0");
        elc.setAttribute(CobolMarkup.IS_SIGNED, "false");

        setCommonNumericAttributes(xsdElement, elc);
    }

    /**
     * Sets attributes for an external decimal.
     * 
     * @param schema the XML schema
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * @param simpleInstanceTD the ECI instance Type Descriptor
     * @throws XsdMappingException if inferring COBOL attributes fails
     */
    protected void setExternalDecimalAttributes(final XmlSchema schema,
            final XmlSchemaElement xsdElement, final Element elc,
            final SimpleInstanceTD simpleInstanceTD) throws XsdMappingException {

        elc.setAttribute(CobolMarkup.TYPE,
                CobolType.ZONED_DECIMAL_ITEM.toString());
        elc.setAttribute(CobolMarkup.USAGE, "DISPLAY");

        /* Set defaults just in case facets are not found */
        int totalDigits = simpleInstanceTD.getSize();
        elc.setAttribute(CobolMarkup.TOTAL_DIGITS,
                Integer.toString(totalDigits));
        elc.setAttribute(CobolMarkup.FRACTION_DIGITS, "0");
        elc.setAttribute(CobolMarkup.IS_SIGNED, "false");

        setCommonNumericAttributes(xsdElement, elc);
    }

    /**
     * All numeric items will have digits and signs that we infer here.
     * <p/>
     * We use XML schema Facets if we find them and recreate a COBOL picture
     * clause.
     * 
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     */
    protected void setCommonNumericAttributes(
            final XmlSchemaElement xsdElement, final Element elc) {

        if (_log.isDebugEnabled()) {
            _log.debug("setCommonNumericAttributes started for element = "
                    + xsdElement.getQName());
        }

        setNumericAttributesFromFacets(xsdElement, elc);

        setNumericPicture(elc);

        if (_log.isDebugEnabled()) {
            _log.debug("setCommonNumericAttributes ended for element = "
                    + xsdElement.getQName());
            _log.debug("   Cobol picture        = "
                    + elc.getAttribute(CobolMarkup.PICTURE));
            _log.debug("   Cobol usage          = "
                    + elc.getAttribute(CobolMarkup.USAGE));
            _log.debug("   Cobol totalDigits    = "
                    + elc.getAttribute(CobolMarkup.TOTAL_DIGITS));
            _log.debug("   Cobol fractionDigits = "
                    + elc.getAttribute(CobolMarkup.FRACTION_DIGITS));
            _log.debug("   Cobol isSigned       = "
                    + elc.getAttribute(CobolMarkup.IS_SIGNED));
        }
    }

    /**
     * Fetches numeric attributes from the ECI XML Schema restriction facets.
     * <p/>
     * Warning, the element is assumed to be a simple type.
     * 
     * @param xsdElement the XML schema element
     * @param elc the DOM Element holding the Cobol annotations
     * 
     * @return true if restriction facets were found
     */
    @SuppressWarnings("unchecked")
    protected void setNumericAttributesFromFacets(
            final XmlSchemaElement xsdElement, final Element elc) {
        XmlSchemaSimpleType xsdType = (XmlSchemaSimpleType) xsdElement
                .getSchemaType();
        XmlSchemaSimpleTypeContent content = xsdType.getContent();
        if (content == null
                || !(content instanceof XmlSchemaSimpleTypeRestriction)) {
            return;
        }
        XmlSchemaSimpleTypeRestriction restriction = (XmlSchemaSimpleTypeRestriction) content;
        if (restriction.getFacets() == null) {
            return;
        }
        XmlSchemaObjectCollection collection = restriction.getFacets();
        for (Iterator < XmlSchemaObject > i = collection.getIterator(); i
                .hasNext();) {
            XmlSchemaObject facet = i.next();
            if (facet instanceof XmlSchemaMinInclusiveFacet) {
                setNumericAttributesFromMinInclusiveFacet(
                        ((XmlSchemaMinInclusiveFacet) facet).getValue()
                                .toString(), elc);
            } else if (facet instanceof XmlSchemaMaxInclusiveFacet) {
                setNumericAttributesFromMaxInclusiveFacet(
                        ((XmlSchemaMaxInclusiveFacet) facet).getValue()
                                .toString(), elc);
            }
        }

    }

    /**
     * We use the min inclusive facet to determine if numeric is signed.
     * 
     * @param minInclusiveValue the min inclusive facet value
     * @param elc the DOM Element holding the Cobol annotations
     */
    protected void setNumericAttributesFromMinInclusiveFacet(
            final String minInclusiveValue, final Element elc) {
        if (minInclusiveValue.startsWith("-")) {
            elc.setAttribute(CobolMarkup.IS_SIGNED, "true");
        }

    }

    /**
     * We use the max inclusive facet to determine the total and fraction digits
     * number.
     * 
     * @param maxInclusiveValue the max inclusive facet value
     * @param elc the DOM Element holding the Cobol annotations
     */
    protected void setNumericAttributesFromMaxInclusiveFacet(
            final String maxInclusiveValue, final Element elc) {
        int totalDigits = 0;
        int fractionDigits = 0;
        int decPos = maxInclusiveValue.indexOf(".");
        if (decPos > -1) {
            fractionDigits = maxInclusiveValue.substring(decPos + 1).length();
            totalDigits = maxInclusiveValue.substring(0, decPos).length()
                    + fractionDigits;
        } else {
            totalDigits = maxInclusiveValue.length();
        }
        elc.setAttribute(CobolMarkup.TOTAL_DIGITS,
                Integer.toString(totalDigits));
        elc.setAttribute(CobolMarkup.FRACTION_DIGITS,
                Integer.toString(fractionDigits));
    }

    /**
     * Once sign, fraction and total number of digits are identified, we can
     * reconstruct a COBOL picture clause.
     * 
     * @param elc the DOM Element holding the Cobol annotations
     */
    protected void setNumericPicture(final Element elc) {
        int totalDigits = Integer.parseInt(elc
                .getAttribute(CobolMarkup.TOTAL_DIGITS));
        int fractionDigits = Integer.parseInt(elc
                .getAttribute(CobolMarkup.FRACTION_DIGITS));
        boolean signed = elc.getAttribute(CobolMarkup.IS_SIGNED).equals("true");

        elc.setAttribute(
                CobolMarkup.PICTURE,
                ((signed) ? "S" : "")
                        + "9("
                        + Integer.toString(totalDigits - fractionDigits)
                        + ((fractionDigits > 0) ? ")V9("
                                + Integer.toString(fractionDigits) : "") + ")");
    }

    /** {@inheritDoc} */
    public void processComplexType(XmlSchema schema,
            XmlSchemaComplexType xsdComplexType, int level)
            throws XsdMappingException {
        /* No handling for java class annotations for now */

    }

    /**
     * ECI ids are composed of a type followed by underscore and then a unique
     * ID..
     * <p/>
     * He we strip the ID and return the ECI type.
     * 
     * @param eciId the ECI ID
     * @return the ECI type
     * @throws XsdMappingException if ID does not follow the expected syntax
     */
    protected String getEciType(final String eciId) throws XsdMappingException {
        if (eciId == null || eciId.indexOf('_') == -1) {
            throw new XsdMappingException("Unrecognized ECI ID " + eciId);
        }
        return eciId.substring(0, eciId.indexOf('_'));
    }

}
